------Math Tutor: Fractions Part I-----
A 4am crack                  2016-01-04
---------------------------------------

Name: Math Tutor: Fractions Part I
Genre: educational
Year: 1987
Authors: Michael Maloney, Michael
  Summers, Pat Stuart
Publisher: Scholastic, Inc.
Media: 2 single-sided 5.25-inch disks
OS: DOS 3.3
Previous cracks: none

Both disks are bootable and protected.
I'll start with disk 1.

                   ~

               Chapter 0
 In Which Various Automated Tools Fail
          In Interesting Ways


COPYA
  immediate disk read error, but it
  gets a participation trophy just for
  showing up

Locksmith Fast Disk Backup
  unable to read any track

EDD 4 bit copy (no sync, no count)
  no errors, but copy loads DOS, runs a
  startup program, and crashes at $90C6

Copy ][+ nibble editor
  all tracks use standard prologues
  (address: D5 AA 96, data: D5 AA AD)
  but modified address epilogues
  (AB FF FF)

Disk Fixer
  ["O" -> "Input/Output Control"]
    set Address Epilogue to "AB FF FF"
  Success! All tracks readable!
  T00 -> looks like a DOS 3.3 RWTS
  T11 -> DOS 3.3 disk catalog
  T01,S09 -> startup program is
    "XMGPRT1"

Why didn't COPYA work?
  modified epilogue bytes (every track)

Why didn't Locksmith FDB work?
  modified epilogue bytes (every track)

Why didn't my EDD copy work?
  probably a nibble check during boot

Next steps:

  1. capture RWTS with AUTOTRACE
  2. convert disk to standard format
     with Advanced Demuffin
  3. find nibble check and bypass it

                   ~

               Chapter 1
In Which We Attempt To Use The Original
    Disk As A Weapon Against Itself


The first step to cracking this disk is
to convert it to a standard format,
meaning standard prologue and epilogue
sequences around each sector. The best
(most robust) way to do this is to use
the original disk's own disk reading
routine to read each sector, then use a
standard disk writing routine to write
out the sector data to a standard disk.

Advanced Demuffin (originally by The
Stack, lightly updated in 2015 by yours
truly, with some convenience functions
and long-standing bug fixes) is the
perfect tool for this kind of sector-
by-sector conversion. It requires me to
provide the original disk's RWTS (disk
reading routines), which are often
stored in $B800..$BFFF.

To automate capturing the RWTS from the
original disk, I wrote a script. When
it works, it works very well; when it
fails, it fails most spectacularly.

[S6,D1=original disk]
[S6,D2=blank disk]
[S5,D1=my work disk]

]PR#5
CAPTURING BOOT0
...reboots slot 6...
...reboots slot 5...
SAVING BOOT0
CAPTURING BOOT1
...reboots slot 6...
...reboots slot 5...
SAVING BOOT1
SAVING RWTS

That worked very well.

]BRUN ADVANCED DEMUFFIN 1.5

["5" to switch to slot 5]

["R" to load a new RWTS module]
  --> At $B8, load "RWTS" from drive 1

["6" to switch to slot 6]

["C" to convert disk]

                 --v--

ADVANCED DEMUFFIN 1.5    (C) 1983, 2014
ORIGINAL BY THE STACK    UPDATES BY 4AM
=======PRESS ANY KEY TO CONTINUE=======
TRK:...................................
+.5:
    0123456789ABCDEF0123456789ABCDEF012
SC0:...................................
SC1:...................................
SC2:...................................
SC3:...................................
SC4:...................................
SC5:...................................
SC6:...................................
SC7:...................................
SC8:...................................
SC9:...................................
SCA:...................................
SCB:...................................
SCC:...................................
SCD:...................................
SCE:...................................
SCF:...................................
=======================================
16SC $00,$00-$22,$0F BY1.0 S6,D1->S6,D2

                 --^--

The disk's own RWTS gave no read errors
on any track. This is the power and the
genius of Advanced Demuffin. Every disk
must be able to read itself. So, let it
read itself, then capture the data and
write it out in a standard format.

Now I have a copy of the disk in a
standard format, which I can read and
inspect with third-party tools.

]PR#5
]CATALOG,S6,D2

C1983 DSR^C#254
033 FREE

 A 003 HELLO
 B 002 BLOAD LANG
 T 002 F1
 B 002 BEGIN
 B 011 C&J
 B 014 P1
 B 009 P2
 B 013 P3
 B 009 P4
 B 010 TABLS
 B 017 EXTRA
 B 013 HANDLE
 B 034 GRAPHIC
 B 007 A------------------A
 B 045 MODULE
 B 044 SECA
 B 044 SECB
 B 044 SECC
 B 044 SECD
 B 044 SECE
 B 044 SECH
 B 006 MATH TUTOR PAGE
 B 002 XMGPRT1

My failed bit copy booted, loaded DOS,
and ran a startup program before
crashing. From my initial inspection
with a sector editor, I gleaned that
the startup program is "XMGPRT1", so
let's start there.

                   ~

               Chapter 2
  In Which We Find A 30-Year-Old Bug


]BLOAD XMGPRT1
]CALL -151

*BF55.BF56

BF55- 00 90

(Diversi-DOS 64K stores the program
start location in $BF55. In standard
DOS 3.3, it's at $AA72.)

*9000L

; get address of RWTS parameter table
9000-   20 E3 03    JSR   $03E3
9003-   85 FB       STA   $FB
9005-   84 FA       STY   $FA

; hmm
9007-   A9 C5       LDA   #$C5
9009-   48          PHA

; copy RWTS parameters
900A-   A9 00       LDA   #$00
900C-   85 FC       STA   $FC
900E-   A2 03       LDX   #$03
9010-   BC 49 90    LDY   $9049,X
9013-   91 FA       STA   ($FA),Y
9015-   CA          DEX
9016-   10 F8       BPL   $9010
9018-   8A          TXA

; hmm
9019-   48          PHA

The X register will be $FF after the
loop, so this means we've pushed $C5FF
to the stack. At this point, a bare RTS
will "return" to $C5FF+1, i.e. reboot.

901A-   20 3C 90    JSR   $903C

*903CL

; call the RWTS (most likely just to
; move the drive head to the proper
; position for an impending nibble
; check)
903C-   20 E3 03    JSR   $03E3
903F-   20 D9 03    JSR   $03D9
9042-   A9 00       LDA   #$00
9044-   85 48       STA   $48

; if that fails, off to The Badlands
9046-   B0 78       BCS   $90C0
9048-   60          RTS

*90C0L

90C0-   A8          TAY
90C1-   DD 88 C0    CMP   $C088,X
90C4-   00          BRK
90C5-   00          BRK
90C6-   00          BRK

Oh, well, there's your problem: you cut
your program short by a few bytes. I'm
guessing there's supposed to be at
least one more instruction there before
it crashes (like something that reboots
either explicitly or through some stack
manipulation). But there isn't, because
someone mis-saved a file 30 years ago.
Always test your failure paths!

Caller was $901A, so resuming at $901D:

*901DL

901D-   A0 01       LDY   #$01
901F-   B1 FA       LDA   ($FA),Y
9021-   AA          TAX
9022-   20 73 90    JSR   $9073

*9073L

; turning on the drive motor manually
; is always suspicious
9073-   BD 89 C0    LDA   $C089,X

; initialize death counters
9076-   A9 56       LDA   #$56
9078-   85 FD       STA   $FD
907A-   A9 08       LDA   #$08
907C-   C6 FC       DEC   $FC
907E-   D0 04       BNE   $9084
9080-   C6 FD       DEC   $FD

; if death counter hits 0, give up
9082-   F0 3C       BEQ   $90C0

; look for an $FB nibble
9084-   BC 8C C0    LDY   $C08C,X
9087-   10 FB       BPL   $9084
9089-   C0 FB       CPY   #$FB
908B-   D0 ED       BNE   $907A
908D-   F0 00       BEQ   $908F

; kill a few cycles (not pointless,
; because the disk spins independently
; of the CPU, so all of these low-level
; disk reads are highly time-sensitive)
908F-   EA          NOP
9090-   EA          NOP

; read data latch (note: no BPL loop
; here, we're just reading it once)
9091-   BC 8C C0    LDY   $C08C,X

; do a compare to set or clear the
; carry bit (among other things, but
; it's the carry bit we care about)
9094-   C0 08       CPY   #$08

; rotate the carry into the low bit of
; the accumulator
9096-   2A          ROL

; if we just rolled a "1" bit out of
; the high bit of the accumulator, take
; this branch
9097-   B0 0B       BCS   $90A4

; next nibble needs to be $FF
9099-   BC 8C C0    LDY   $C08C,X
909C-   10 FB       BPL   $9099

; ...otherwise we start over
909E-   C0 FF       CPY   #$FF
90A0-   D0 D8       BNE   $907A

; loop back to get next nibble
90A2-   F0 EB       BEQ   $908F

; execution continues here (from $9097)
; get another nibble
90A4-   BC 8C C0    LDY   $C08C,X
90A7-   10 FB       BPL   $90A4

; stash it in zero page
90A9-   84 FC       STY   $FC

; if the accumulator is anything but
; %00001010, start over
90AB-   C9 0A       CMP   #$0A
90AD-   D0 CB       BNE   $907A

I got lost several times trying to
follow this routine. I think the
easiest way to explain it is to show
the difference between the original
disk and my non-working copy.

Here is the original disk, as seen by
the Copy II+ nibble editor. Nibbles
with extra "0" bits (timing bits) after
them are displayed in inverse on an
original machine, marked here with a
"+" after the nibble.

                 --v--

   COPY ][ PLUS BIT COPY PROGRAM 8.4
(C) 1982-9 CENTRAL POINT SOFTWARE, INC.
---------------------------------------

TRACK:     START: 1B1E  LENGTH: 17C1

1C70: 9F EB E5 FC D7 D7 D7 EE   VIEW
1C78: FA E6 E6 FF FE F2 ED FD
1C80: FF EF ED BA BB DD AF E6
1C88: B7 A7 CB B7 DE AA EB FF
1C90: FF FF FF FB+FF FF+FF FF+
1C98: FD FF+FF+FF+FF+FF+FF+FF+
1CA0: FF+FF+D5 AA 96 AA AB AA
1CA8: AA AA AB AA AA AB FF FF+
1CB0: FF+FF+FF+FF+FF+FF D5 AA
---------------------------------------

  A  TO ANALYZE DATA  ESC TO QUIT

  ?  FOR HELP SCREEN  /  CHANGE PARMS

  Q  FOR NEXT TRACK   SPACE TO RE-READ

                 --^--

It's easy to understand why a simple
sector copy failed. The sequence that
this code is looking for starts at
offset $1C93, which is between the end
of one sector and the beginning of the
next. (The data epilogue is at $1C8C;
the next address prologue is at $1CA2.)
Sector copiers discard everything
between those delimiters and rebuild
the track with a default pattern of
sync bytes. That pattern doesn't
include an $FB nibble, so the nibble
check fails.

But the EDD bit copy also failed. Here
is the original disk's pattern at
offset $1C93:

  - $FB + timing bit
  - $FF
  - $FF + timing bit
  - $FF
  - $FF + timing bit

And here is what the same part of the
track looks like on my failed EDD copy:

                 --v--

   COPY ][ PLUS BIT COPY PROGRAM 8.4
(C) 1982-9 CENTRAL POINT SOFTWARE, INC.
---------------------------------------

TRACK:     START: 1B1E  LENGTH: 17C1

1C70: 9F EB E5 FC D7 D7 D7 EE   VIEW
1C78: FA E6 E6 FF FE F2 ED FD
1C80: FF EF ED BA BB DD AF E6
1C88: B7 A7 CB B7 DE AA EB FF
1C90: FF FF FF FB+FF FF FF+FF+
1C98: FD FF+FF+FF+FF+FF+FF+FF+
1CA0: FF+FF+D5 AA 96 AA AB AA
1CA8: AA AA AB AA AA AB FF FF+
1CB0: FF+FF+FF+FF+FF+FF D5 AA
---------------------------------------

  A  TO ANALYZE DATA  ESC TO QUIT

  ?  FOR HELP SCREEN  /  CHANGE PARMS

  Q  FOR NEXT TRACK   SPACE TO RE-READ

                 --^--

A subtle difference! The sequence at
offset $1C93 now looks like this:

  - $FB + timing bit
  - $FF
  - $FF
  - $FF + timing bit
  - $FF + timing bit

This code is looking for $FF bytes with
an alternating pattern of timing bit,
no timing bit, timing bit, no timing
bit. (This ends up as a bit pattern in
the accumulator.) It doesn't find that
on the bit copy, so it knows it's not
running on an original disk.

                   ~

               Chapter 3
    In Which We Stare Into Infinity
       And Infinity Stares Back
      And Executes A DOS Command


Continuing from $90AF...

*90AFL

; get a nibble
90AF-   BD 8C C0    LDA   $C08C,X
90B2-   10 FB       BPL   $90AF

; more bit twiddling
90B4-   38          SEC
90B5-   2A          ROL

; AND it with the previously stashed
; nibble
90B6-   25 FC       AND   $FC
90B8-   49 FF       EOR   #$FF

; branch to failure path
90BA-   D0 04       BNE   $90C0

; success path falls through to here --
; turn off the drive motor and return
; to the caller
90BC-   DD 88 C0    CMP   $C088,X
90BF-   60          RTS

This was called from $9022, so let's
continue from $9025.

*9025L

; pop the bogus return value ($C5FF)
; off the stack
9025-   68          PLA
9026-   68          PLA

; Route a series of bytes through the
; DOS output vector at ($36). This is
; how binary files "execute" DOS 3.3
; commands, like PRINT CHR$(4)"..." in
; Applesoft BASIC. This is totally
; legitimate code, cleverly disguised
; as an infinite loop.
9027-   A0 00       LDY   #$00
9029-   84 FE       STY   $FE
902B-   B9 4D 90    LDA   $904D,Y
902E-   09 80       ORA   #$80
9030-   20 39 90    JSR   $9039
9033-   A4 FE       LDY   $FE
9035-   C8          INY
9036-   4C 29 90    JMP   $9029
9039-   6C 36 00    JMP   ($0036)

Let's see what it's printing.

*FC58G N 400<904D.9072M

DRUN HELLO                          M@
^                control characters ^^

Aha! It's not an infinite loop after
all. Well, it is, technically, but it
won't actually run forever. It's not
easy to show in plain text, but the
initial "D" and the final "M@" are in
inverse. So that string starts with
Ctrl-D and ends with Ctrl-M and a null
byte. Since DOS is already loaded,
printing this through the DOS vector
will execute that command, as if you
typed it from a prompt yourself. Since
that program never returns to the
caller, it will break out of the
seemingly "infinite" loop.

Since there are no side effects to the
copy protection routine, I can change
this file to simply skip the call to
$9059 and continue on to running the
real startup program. Using my trusty
Disk Fixer sector editor, I press "D"
for directory mode, select "XMGPRT1",
and I find this code on T16,S00.

Disk 1:
T16,S00,$26 change 20 to 2C

Disk 2 has identical protection, but a
different layout of files on disk, so
the patch is on T16,S0E.

Disk 2:
T16,S0E,$26 change 20 to 2C

Quod erat liberandum.

                   ~

               Epilogue
  In Which We Stop And Smell The RWTS


Astute readers may notice that we
converted the disk to a standard format
(with Advanced Demuffin), but then we
didn't need to patch the RWTS of the
copy in order to read itself. Here is
how that works.

]PR#5
...
]BLOAD RWTS,A$3800
]CALL -151

*FE89G FE93G     ; disconnect DOS
*B800<3800.3FFFM ; move RWTS into place

*B98BL

; usually the code to match the address
; epilogue
B98B-   4C 16 BD    JMP   $BD16

Wait, what?

*BD16L

; accept either $AB or $DE as the first
; epilogue nibble
BD16-   BD 8C C0    LDA   $C08C,X
BD19-   10 FB       BPL   $BD16
BD1B-   C9 AB       CMP   #$AB
BD1D-   F0 06       BEQ   $BD25
BD1F-   C9 DE       CMP   #$DE
BD21-   F0 02       BEQ   $BD25

; didn't find either -- set carry to
; indicate a disk read error, and exit
; via RTS
BD23-   38          SEC
BD24-   60          RTS

; Execution continues here (from either
; $BD1D or $BD21, depending on which
; nibble we found). Now we'll accept
; either $FF or $AA as the second
; epilogue nibble.
BD25-   BD 8C C0    LDA   $C08C,X
BD28-   10 FB       BPL   $BD25
BD2A-   C9 FF       CMP   #$FF
BD2C-   F0 04       BEQ   $BD32
BD2E-   C9 AA       CMP   #$AA
BD30-   D0 F1       BNE   $BD23
BD32-   18          CLC
BD33-   60          RTS

So this disk's RWTS will accept either
"AB FF" or "DE AA". (Technically, it
would also accept "AB AA" or "DE FF",
but neither is used on this disk.) The
third epilogue nibble is never checked.

At the same time, the write part of the
RWTS always uses the standard "DE AA
EB" epilogues. The result: a single
RWTS that can read the program disk
(which uses "AB FF" epilogues) and
standard disks (with the "DE AA"
epilogues). This allows the program to
initialize and store user data on a
standard formatted disk, without the
hassle of a mode-switching routine that
changes the RWTS code in memory between
protected and unprotected mode. There
is only one mode, and since one of its
features is being able to read standard
formatted disks, no RWTS patches are
required.

---------------------------------------
A 4am crack                     No. 550
------------------EOF------------------
